#include "QRenderer.h"
#include "QDynQml.h"
#include "QUtil.h"
#include "QDynQml.h"
#include <QQuickItemGrabResult>
#include "../../MgrController.h"
#include "../../core/CElapsedTimer.h"
#include "../../test/TestCase.h"

P TestCase::s_debugModelPos;

/*******************************************************************************************/
/***************************** ThreadRenderer **********************************************/
/*******************************************************************************************/
QRenderer* QRenderer::sInstance = nullptr;

void QRenderer::setItemVisible(bool visible) {
	if (!sInstance) return;
	sInstance->setVisible(visible);
	Cache::setReceivingData(visible);
	if (!visible)/**@note hide zhichePng immediately, not wait until the round for rendering zhiche entity*/
		QDynQml::get()->setZhichePngVisible(false);
}

/**@note in MainGUIThread*/
QRenderer::QRenderer() {
	setFlag(ItemHasContents, true);
	setWidth(W3D);
	setHeight(H3D);
	if (!IsEnv_Linux)
		setFocus(true);// for keyboard input on debug test

	sInstance = this;
	m_renderThread = new QRenderThread;
}

/**@note in QSGRenderThread*/
QSGNode *QRenderer::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {/**@link https://doc.qt.io/archives/qt-5.12/qquickitem.html#updatePaintNode */
	auto node = static_cast<QTextureNode*>(oldNode);
	if (!m_renderThread->glContext) {
		auto current = window()->openglContext();
		/**
		 * @note Some GL implementations requres that the currently bound context is made non-current before we set up sharing,
		 *       so we doneCurrent here and makeCurrent down below while setting up our own context.
		 */
		current->doneCurrent();

		m_renderThread->glContext = new QOpenGLContext();
		auto format = current->format();
		format.setSamples(Config::Render_SurfaceSamples);// request an OpenGL context that supports 8x oversampling, may not supported by opengl, but a request should be send to check!
		m_renderThread->glContext->setFormat(format);
		m_renderThread->glContext->setShareContext(current);
		m_renderThread->glContext->create();
		m_renderThread->glContext->moveToThread(m_renderThread);

		current->makeCurrent(window());

		qInfo() << "====== OpenGL/ES Context ======\n"
				<< m_renderThread->glContext
				<< "\n*****************************************"
				<< "\n*****************************************"
				<< "\n*****************************************";

		QUtil::doInUiThread([&] {/**@note in GUI-MainThread*/
			m_renderThread->offscreenSurface = new QOffscreenSurface;
			m_renderThread->offscreenSurface->setFormat(m_renderThread->glContext->format());
			m_renderThread->offscreenSurface->create();// should create in GUI-MainThread, otherwise: 'Attempting to create QWindow-based QOffscreenSurface outside the gui thread. Expect failures.'

			m_renderThread->moveToThread(m_renderThread);// should use in MyRenderThread
			m_renderThread->start();

			connect(window(), &QQuickWindow::sceneGraphInvalidated, m_renderThread, &QRenderThread::shutdown, Qt::QueuedConnection);

			captureWindow();

			QQuickItem::update();
		});
		return nullptr;// called once	
	}

	if (!node) {
		node = new QTextureNode(window());

		/**
		 * @brief Set up connections to get the production of FBO textures in sync with vsync on the rendering thread.
		 *
		 * When a new texture is ready on the rendering thread,
		 * we use a direct connection to the texture node to let it know a new texture can be used.
		 * The node will then emit pendingNewTexture which we bind to QQuickWindow::update to schedule a redraw.
		 *
		 * When the scene graph starts rendering the next frame, the prepareNode() function is used to update the node with the new texture.
		 * Once it completes, it emits textureInUse() which we connect to the FBO rendering thread's renderNext() to have it start producing content into its current "back buffer".
		 *
		 * This FBO rendering pipeline is limited by vsync on the scene graph rendering thread.
		 *
		 * @param Qt::AutoConnection default: if sender's thread == receiver's thread <=> Qt::DirectConnection, otherwise <=> Qt::QueuedConnection
		 * @param Qt::DirectConnection slot execute in sender's thread
		 * @param Qt::QueuedConnection slot execute in reveiver's thread
		 */
		connect(window(), &QQuickWindow::beforeRendering, node, &QTextureNode::prepareNode, Qt::DirectConnection);//#1
		connect(node, &QTextureNode::textureInUse, m_renderThread, &QRenderThread::renderNext, Qt::QueuedConnection);//#2
		connect(m_renderThread, &QRenderThread::textureReady, node, &QTextureNode::newTexture, Qt::DirectConnection);//#3
		connect(node, &QTextureNode::pendingNewTexture, window(), &QQuickWindow::update, Qt::QueuedConnection);//#4

		// Get the production of FBO textures started..
		QMetaObject::invokeMethod(m_renderThread, &QRenderThread::renderNext, Qt::QueuedConnection);
	}
	node->setRect(boundingRect());
	return node;// just called once throughout the entire period
}

void QRenderer::keyPressEvent(QKeyEvent *event) {
	const float deltaX = 0.4;
	const float deltaZ = 1.0;
	switch (event->key()) {
		case Qt::Key_Left: TestCase::debugModelX(-deltaX); break;
		case Qt::Key_Right: TestCase::debugModelX(deltaX); break;
		case Qt::Key_Up: TestCase::debugModelZ(-deltaZ); break;
		case Qt::Key_Down: TestCase::debugModelZ(deltaZ); break;
		default: QQuickItem::keyPressEvent(event); break;
	}
}

void QRenderer::keyReleaseEvent(QKeyEvent *event) {
	/*P rayFromL = P(-6.24229, 11.2544), rayToL = P(-47.4843, -119.21);
	P rayFromR = P(6.24229, 11.2544), rayToR = P(47.4843, -119.21);
	bool onCCWSideL = checkIfOnRayCCWSweepingSide(TestCase::debugModelPos(), rayFromL, rayToL);
	bool onCCWSideR = checkIfOnRayCCWSweepingSide(TestCase::debugModelPos(), rayFromR, rayToR);
	std::cout << "onCCWSide => (L:" << std::boolalpha << onCCWSideL << ", R:" << onCCWSideR << ")" << std::endl;*/
}

void QRenderer::captureWindow() {
	if (!TestJsonData::isRecordingWindow()) return;
	const CString& path = IsEnv_Linux ? "res3D/screenshots" : "C:/Users/huabao/Desktop/screenshots";
	CUtil::mkdirIfNotExists(path);
	/**
	 * FFMPEG: -framerate 1 => 1FPS => 1s/frame
	 * @param framerate = 1/frameIntervalSeconds
	 * ffmpeg -framerate 2 -i %d.jpg -c:v libx264 -pix_fmt yuv420p output.mp4
	 */
	const int GrabWindowIntervalMs = 100;
	QUtil::loopExecute([=] {
		if (!TestJsonData::isRecordingWindow()) return;
		static int cnt = 0;
		auto result = this->window()->grabWindow();// must be called in Main-GUI thread
		if (result.isNull()) return;
		result.save((path + "/" + ++cnt + ".jpg").c_str());
	}, GrabWindowIntervalMs);
}

/*******************************************************************************************/
/***************************** MyRenderThread **********************************************/
/*******************************************************************************************/
/**@note in MyRenderThread(async called from QSGRenderThread)*/
void QRenderThread::renderNext() {// Q_SLOTS
	if (!Scene::isInitialized()) {
		bool success = glContext->makeCurrent(offscreenSurface);// make current should be called in correct thread
		ASSERT(success, "GLContext makeCurrent fail!");

		Scene::init(new QtGLProcLoader(glContext));

		TestCase::run();
	}

	FBO* frontFBO = MgrController::updateAllEntities();

	emit textureReady(frontFBO->texture);
}

void QRenderThread::shutdown() {// Q_SLOTS
	glContext->makeCurrent(offscreenSurface);
	//delete frontFBO and backFBO;
	glContext->doneCurrent();
	delete glContext;
	// schedule this to be deleted only after we're done cleaning up
	offscreenSurface->deleteLater();
	// Stop event processing, move the thread to GUI and make sure it is deleted.
	QThread::exit();
	this->moveToThread(qApp->thread());
}

/*******************************************************************************************/
/***************************** QTextureNode *************************************************/
/*******************************************************************************************/
/**@note in OSGRenderThread*/
QTextureNode::QTextureNode(QQuickWindow *window) : m_texture(0), m_osgTexture(nullptr), m_window(window) {
	// our texture node must have a texture, so use the default 0 texture.
	QImage transparent(1, 1, QImage::Format_RGBA8888);
	transparent.fill(Qt::transparent);// init to transparent
	m_osgTexture = m_window->createTextureFromImage(transparent);
	m_osgTexture->setFiltering(QSGTexture::Linear);
	m_osgTexture->setMipmapFiltering(QSGTexture::Linear);
	QSGSimpleTextureNode::setTexture(m_osgTexture);
	QSGSimpleTextureNode::setTextureCoordinatesTransform(QSGSimpleTextureNode::MirrorVertically);
	QSGSimpleTextureNode::setFiltering(QSGTexture::Linear);
}

QTextureNode::~QTextureNode() {
	delete m_osgTexture;
}

/*@note in OSGRenderThread*/
void QTextureNode::prepareNode() {// Q_SLOTS (signal: QQuickWindow::beforeRendering)
	if (ShowFPS) {
		static CElapsedTimer m_frameTimer;
		m_frameTotMs += m_frameTimer.elapsed();
		if (++m_frameCnt % FPSAverageFrames == 0) {
			QDynQml::get()->setFPS(1000 / m_frameTotMs * FPSAverageFrames);
			m_frameTotMs = 0;
		}
		m_frameTimer.reset();
	}

	m_mutex.lock();// because of different thread(OSGRenderThread and MyRenderThread) operate the same variable)
	int newTexture = m_texture;
	m_texture = 0;
	m_mutex.unlock();
	if (newTexture) {
		delete m_osgTexture;
		m_osgTexture = m_window->createTextureFromId(newTexture, QSize(W3D, H3D), QQuickWindow::TextureHasAlphaChannel);// the rendered content has alpha
		QSGSimpleTextureNode::setTexture(m_osgTexture);

		QSGSimpleTextureNode::markDirty(DirtyMaterial);// Notifies all connected renderers that the node has dirty bits.

		// This will notify the rendering thread that the texture is now being rendered and it can start rendering to the other one.
		emit textureInUse();
	}
}

/**@note in MyRenderThread*/
void QTextureNode::newTexture(int texture) {// Q_SLOTS
	m_mutex.lock();
	m_texture = texture;
	m_mutex.unlock();
	// we cannot call QQuickWindow::update directly here, as this is only allowed from the rendering thread or GUI thread.
	emit pendingNewTexture();
}